﻿// HSS.Interlink.UI.UploadManager.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.html)
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2012
// ----------------------------------------------------------------------------
// File:       UploadManager.cs
// Author:     HSS\gbanta
// Created:    01/17/2012
// Modified:   01/17/2012
// ----------------------------------------------------------------------------
namespace HSS.Interlink.UI
{
	#region Using Directives
	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.Linq;
	using System.Threading;
	using System.Windows.Threading;


	#endregion

	#region UploadManager
	/// <summary>
	/// A service for managing the upload of one or more files.
	/// </summary>
	public sealed class UploadManager
	{
		#region Events

		#region UploadProgressChanged
		/// <summary>
		/// Occurs when the upload progress changes.
		/// </summary>
		public event EventHandler<UploadManagerProgressEventArgs> UploadProgressChanged;
		private void ReportUploadProgressChanged(UploadManagerProgressEventArgs args)
		{
			var handler = UploadProgressChanged;
			if (null != handler)
				handler(null, args);
		}
		private void ReportUploadProgressChanged(object arg)
		{
			this.ReportUploadProgressChanged((UploadManagerProgressEventArgs)arg);
		}
		private void PostProgressChanged()
		{
			// HACK: need to find a better way to capture bytes sent.
			this.totalBytesSent = this.Files.Sum(f => f.BytesUploaded);
			if (this.totalBytes > 0)
			{
				var pct = (int)((this.totalBytesSent * 100) / this.totalBytes);
				this.dispatcher.BeginInvoke(this.uploadProgressChanged, new UploadManagerProgressEventArgs(pct, null, this.totalBytesSent, this.totalBytes));
			}
		}
		#endregion

		#region UploadCompleted
		/// <summary>
		/// Occurs when the upload completes.
		/// </summary>
		public event EventHandler UploadCompleted;
		private void OnUploadCompleted(EventArgs e)
		{
			var handler = UploadCompleted;
			if (null != handler)
				handler(this, e);
		}
		private void OnUploadAsyncCompleted(object arg)
		{
			this.OnUploadCompleted((EventArgs)arg);
		}
		private void InvokeOperationCompeted(Dispatcher uiDispatch, SendOrPostCallback callback, EventArgs eventArgs)
		{
			this.IsBusy = false;
			this.CompleteUploadClientState();
			uiDispatch.BeginInvoke(callback, eventArgs);
		}
		#endregion

		#region FileAdded
		/// <summary>
		/// Occurs when a file is added.
		/// </summary>
		public event EventHandler<UploadManagerFileChangedEventArgs> FileAdded;
		private void OnFileAdded(UploadManagerFileChangedEventArgs args)
		{
			var handler = this.FileAdded;
			if (null != handler)
				handler(this, args);
		}
		private void OnFileAdded(object args)
		{
			this.OnFileAdded((UploadManagerFileChangedEventArgs)args);
		}
		private static void InvokeFileAdded(Dispatcher dispatcher, SendOrPostCallback callback, UploadManagerFileChangedEventArgs eventArgs)
		{
			dispatcher.BeginInvoke(callback, eventArgs);
		}
		#endregion

		#region FileRemoved
		/// <summary>
		/// Occurs when a file is removed.
		/// </summary>
		public event EventHandler<UploadManagerFileChangedEventArgs> FileRemoved;
		private void OnFileRemoved(UploadManagerFileChangedEventArgs args)
		{
			var handler = this.FileRemoved;
			if (null != handler)
				handler(this, args);
		}
		private void OnFileRemoved(object args)
		{
			this.OnFileRemoved((UploadManagerFileChangedEventArgs)args);
		}
		private static void InvokeFileRemoved(Dispatcher dispatcher, SendOrPostCallback callback, UploadManagerFileChangedEventArgs eventArgs)
		{
			dispatcher.BeginInvoke(callback, eventArgs);
		}
		#endregion

		#region FileChanged
		/// <summary>
		/// Occurs when a file has changes.
		/// </summary>
		public event EventHandler<UploadManagerFileChangedEventArgs> FileChanged;
		private void OnFileChanged(UploadManagerFileChangedEventArgs args)
		{
			var handler = this.FileChanged;
			if (null != handler)
				handler(this, args);
		}
		private void OnFileChanged(object args)
		{
			this.OnFileChanged((UploadManagerFileChangedEventArgs)args);
		}
		private static void InvokeFileChanged(Dispatcher dispatcher, SendOrPostCallback callback, UploadManagerFileChangedEventArgs eventArgs)
		{
			dispatcher.BeginInvoke(callback, eventArgs);
		}
		#endregion

		#endregion

		#region Fields
		private SendOrPostCallback uploadOperationCompleted;
		private SendOrPostCallback uploadProgressChanged;
		private SendOrPostCallback uploadFileAdded;
		private SendOrPostCallback uploadFileRemoved;
		private SendOrPostCallback uploadFileChanged;
		private Queue<UploadFile> uploadFiles;
		private Dispatcher dispatcher;
		private bool initUploadClientAsync;
		private bool isCanceled;
		private int callNesting;
		private int currentCount;
		private long totalBytesSent;
		private long totalBytes;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="uiDispatcher">The UI Dispatcher to raise events on.</param>
		/// <param name="maximumConcurrency">The maximum number of concurrent files that can be upload at a time.</param>
		public UploadManager(Dispatcher uiDispatcher, int maximumConcurrency)
		{
			this.Files = new ObservableCollection<UploadFile>();
			this.Files.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Files_CollectionChanged);

			this.dispatcher = uiDispatcher;
			this.MaximumConcurrentUpload = maximumConcurrency;

			this.InitUploadClientState();

		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets a the <see cref="ObservableCollection{IUploadFile}"/> of files being uploaded.
		/// </summary>
		public ObservableCollection<UploadFile> Files
		{
			get;
			private set;
		}
		/// <summary>
		/// Gets if the upload service is currently uploading files.
		/// </summary>
		public bool IsBusy
		{
			get;
			private set;
		}
		/// <summary>
		/// Gets or sets the maximum number of concurrent files allowed to be uploaded at a time. Default: 2
		/// </summary>
		public int MaximumConcurrentUpload
		{
			get;
			set;
		}
		/// <summary>
		/// Gets the count of files that have been upload so far (Canceled, Errored or Finished).
		/// </summary>
		public int CompletedCount
		{
			get { return this.Files.Count(f => f.State == FileState.Canceled || f.State == FileState.Error || f.State == FileState.Finished); }
		}
		#endregion

		#region Methods

		/// <summary>
		/// Resets the list of files and any upload statistics.
		/// </summary>
		public void Reset()
		{
			if (!this.IsBusy)
				this.Clear();
		}
		/// <summary>
		/// Begins the upload process.
		/// </summary>
		public void UploadAsync()
		{
			this.ClearUploadClientState();

			this.IsBusy = true;

			foreach (var file in this.Files)
			{
				if (file.Prepare())
				{
					this.uploadFiles.Enqueue(file);
					this.totalBytes += file.FileLength;
				}
			}

			if (this.uploadFiles.Count > 0)
				this.ContinueUpload();
			else
				this.InvokeOperationCompeted(this.dispatcher, this.uploadOperationCompleted, EventArgs.Empty);
		}

		/// <summary>
		/// Attempts to cancel the current upload operation.
		/// </summary>
		public void CancelAsync()
		{
			if (isCanceled)
				return;
			isCanceled = true;

			foreach (var file in this.Files)
				file.CancelAsync();
		}

		private void ContinueUpload()
		{
			if (this.CompletedCount == this.Files.Count)
			{
				this.InvokeOperationCompeted(this.dispatcher, this.uploadOperationCompleted, EventArgs.Empty);
			}
			else
			{
				while (this.uploadFiles.Count > 0)
				{
					var file = this.uploadFiles.Dequeue();
					if (file.State == FileState.Pending)
					{
						Interlocked.Increment(ref currentCount);
						file.IsSelected = true;
						file.UploadAsync();
						if (currentCount >= this.MaximumConcurrentUpload)
							return;
					}
				}
			}
		}
		private void InitUploadClientState()
		{
			if (!this.initUploadClientAsync)
			{
				this.uploadOperationCompleted = new SendOrPostCallback(this.OnUploadAsyncCompleted);
				this.uploadProgressChanged = new SendOrPostCallback(this.ReportUploadProgressChanged);
				this.uploadFileAdded = new SendOrPostCallback(this.OnFileAdded);
				this.uploadFileRemoved = new SendOrPostCallback(this.OnFileRemoved);
				this.uploadFileChanged = new SendOrPostCallback(this.OnFileChanged);
				this.uploadFiles = new Queue<UploadFile>();
				this.initUploadClientAsync = true;
			}
		}
		private void ClearUploadClientState()
		{
			if (AnotherCallInProgress(Interlocked.Increment(ref this.callNesting)))
			{
				this.CompleteUploadClientState();
				throw new NotSupportedException("Concurrent upload requests are not supported.");
			}
			this.uploadFiles.Clear();
			this.isCanceled = false;
			this.currentCount = 0;
			this.totalBytesSent = 0;
			this.totalBytes = 0;
		}
		private void CompleteUploadClientState()
		{
			Interlocked.Decrement(ref this.callNesting);
		}
		private static bool AnotherCallInProgress(int nesting)
		{
			return (nesting > 1);
		}
		private void Clear()
		{
			foreach (var item in this.Files)
			{
				var file = item as UploadFile;
				if (null != file)
				{
					file.PropertyChanged -= new System.ComponentModel.PropertyChangedEventHandler(File_PropertyChanged);
					file.UploadProgressChanged -= new EventHandler<UploadFileProgressEventArgs>(File_UploadProgressChanged);
					file.UploadCompleted -= new EventHandler<UploadFileCompletedEventArgs>(File_UploadCompleted);
					InvokeFileRemoved(this.dispatcher, this.uploadFileRemoved, new UploadManagerFileChangedEventArgs(file, ""));
				}
			}
			this.Files.Clear();
		}

		#endregion

		#region Event Handlers
		void Files_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
		{
			if (this.IsBusy)
				throw new NotSupportedException("Cannot modify the files collection once the upload has begun.");

			if (null != e.OldItems && e.OldItems.Count > 0)
			{
				foreach (var item in e.OldItems)
				{
					var file = item as UploadFile;
					file.PropertyChanged -= File_PropertyChanged;
					file.UploadProgressChanged -= File_UploadProgressChanged;
					file.UploadCompleted -= File_UploadCompleted;
					InvokeFileRemoved(this.dispatcher, this.uploadFileRemoved, new UploadManagerFileChangedEventArgs(file, ""));
				}
			}

			if (null != e.NewItems && e.NewItems.Count > 0)
			{
				foreach (var item in e.NewItems)
				{
					var file = item as UploadFile;
					file.PropertyChanged += File_PropertyChanged;
					file.UploadProgressChanged += File_UploadProgressChanged;
					file.UploadCompleted += File_UploadCompleted;
					InvokeFileAdded(this.dispatcher, this.uploadFileAdded, new UploadManagerFileChangedEventArgs(file, ""));
				}
			}
		}
		void File_UploadCompleted(object sender, UploadFileCompletedEventArgs e)
		{
			Interlocked.Decrement(ref this.currentCount);
			this.ContinueUpload();
		}
		void File_UploadProgressChanged(object sender, UploadFileProgressEventArgs e)
		{
			this.PostProgressChanged();
		}
		void File_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
		{
			var model = sender as UploadFile;

			switch (e.PropertyName)
			{
				case "State":
					switch (model.State)
					{
						case FileState.Removed:
							this.Files.Remove(model);
							break;
					}
					break;

				default:
					InvokeFileChanged(this.dispatcher, this.uploadFileChanged, new UploadManagerFileChangedEventArgs(model, e.PropertyName));
					break;
			}
		}
		#endregion

	}
	#endregion
}